require 'apr.oo'
require 'apr.std.file'

--- Create a file-like object that backends on a table of strings for more efficient appending
-- @param startval a starting value for the buffer - either a filelike object (file, socket, or other StringBuffer), an iterator function, or a string.  Leave off for no starting value
apr.StringBuffer = apr.oo.Class(function(self, startval)
    self.back = {}
    self.backstops = {}
    self.pos = 0
    self.size = 0
    -- same as self.pos, but break apart into the key in self.back and the position relative to that index string
    self.backpos = {0, 0}
    
    if apr.std.file.isfilelike(startval) then
        self:stream(startval)
    elseif type(startval) == 'function' then
        for v in startval() do
            self:write(v)
        end
    elseif startval ~= nil then
        self:write(startval)
    end
end)

function apr.StringBuffer:len()
    return self.size
end

function apr.StringBuffer:read(...)
    local reading = {}
    local formats = {...}
    if #formats == 0 then formats = {'*l'} end
    
    for _, formatstr in ipairs(formats) do
        local curindex, curpos = unpack(self.backpos)
        local curblock = self.back[curindex]
        
        if self.pos == self.size then
            --No matter what, reading at the end of the buffer returns nil
            table.insert(reading, nil)
        elseif formatstr == '*n' then
            --Read until you get a non-numeric string
            local bufnum = ''
            while self.pos < self.size do
                local start, stop = curblock:find('^[%d%+%-%.]+', curpos)
                if start == nil then break end
                
                local nextpart = curblock:sub(start, stop)
                bufnum = bufnum .. nextpart
                self:seek('cur', nextpart:len())
                curindex, curpos = unpack(self.backpos)
                curblock = self.back[curindex]
            end
            
            --Since some non-numbers (like -- or 0.++.0) match the pattern above, start at the end and work backwards until the string is empty or tonumber() works
            local trynum = nil
            while trynum == nil and bufnum:len() > 0 do
                trynum = tonumber(bufnum)
                if trynum == nil then
                    bufnum = bufnum:sub(1, -2)
                    self:seek('cur', -1)
                end
            end
            
            table.insert(reading, trynum)
        elseif formatstr == '*a' then
            --Read from the current position to the end of the buffer
            local allbuff = curblock:sub(curpos)
            table.insert(reading, allbuff .. table.concat(self.back, curindex + 1))
            self:seek('end')
        elseif type(formatstr) == 'number' then
            --Read formatstr number of bytes
            if formatstr <= 0 then
                table.insert(reading, nil)
            elseif formatstr > self.size - self.pos then
                local readall = self:read('*a')
                table.insert(reading, readall)
            elseif formatstr <= curblock:len() - curpos + 1 then
                table.insert(reading, curblock:sub(curpos, curpos + formatstr - 1))
                self:seek('cur', formatstr)
            else
                local toread = formatstr
                local bytebuffer = {}
                while toread > 0 do
                    local curindex, curpos = unpack(self.backpos)
                    local curblock = self.back[curindex]
                    local nextbytes = math.min(toread, curblock:len() - curpos  + 1)
                    local readbytes = self:read(nextbytes)
                    table.insert(bytebuffer, readbytes)
                    toread = toread - nextbytes
                    self:seek('cur', nextbytes)
                end
                table.insert(reading, table.concat(bytebuffer, ''))
            end
        else
            --Read until the next newline
            local bufline = {}
            while curblock ~= nil and self.pos < self.size do
                local breakpos = curblock:find('\n', curpos)
                if breakpos ~= nil then
                    table.insert(bufline, curblock:sub(curpos, breakpos - 1))
                    self:seek('cur', breakpos - curpos)
                    break
                else
                    table.insert(bufline, curblock:sub(curpos))
                    self:seek('cur', curblock:len() - curpos + 1)
                    
                    curindex, curpos = unpack(self.backpos)
                    curblock = self.back[curindex]
                end
            end
            
            if #bufline == 0 then
                table.insert(reading, nil)
            else
                table.insert(reading, table.concat(bufline, ''))
            end
        end
    end
    
    return unpack(reading)
end

function apr.StringBuffer:write(...)
    for _, val in ipairs({...}) do
        if val ~= '' and val ~= nil then
            val = tostring(val)
            local vallen = val:len()

            --Writes always occur at the end of the buffer
            self.pos = self.size

            table.insert(self.back, val)
            local inspos = #self.back
            self.backstops[inspos] = self.pos + 1
            self.backpos[1] = inspos
            self.backpos[2] = vallen
            self.pos = self.pos + vallen
            self.size = self.pos
        end
    end
end

function apr.StringBuffer:lines()
    return function()
        return self:read()
    end
end

function apr.StringBuffer:seek(whence, offset)
    offset = offset or 0
    local changed = false
    
    if whence == 'set' and offset ~= self.pos then
        changed = true
        self.pos = offset
    elseif whence == 'cur' and offset ~= 0 then
        changed = true
        self.pos = self.pos + offset
    elseif whence == 'end' and self.pos ~= self.size + offset then
        changed = true
        self.pos = self.size + offset
    end
    
    if changed then
        if self.pos < 0 then self.pos = 0 elseif self.pos > self.size then self.pos = self.size end
        self:set_backpos()
    end
    return self.pos
end

function apr.StringBuffer:set_backpos()
    local backindex = nil
    local backstart = nil
    for index, startpos in ipairs(self.backstops) do
        if startpos > self.pos + 1 then break end
        backindex = index
        backstart = startpos
    end
    
    self.backpos[1] = backindex
    self.backpos[2] = self.pos - backstart + 2
end

function apr.StringBuffer:getvalue()
    return table.concat(self.back, '')
end

apr.StringBuffer.stream = apr.std.file.stream
apr.StringBuffer.__tostring = apr.StringBuffer.getvalue
